/*
 * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include "esp_tls.h"
#include "esp_http_client.h"
#include "esp_log.h"
#include "cJSON.h"
#include <freertos/FreeRTOS.h>
#include <freertos/timers.h>
#include "mbedtls/base64.h"
#include "esp_crt_bundle.h"
#include "esp_heap_caps.h"

#include "http_face_match.h"

#define MIN(x, y) (((x) < (y)) ? (x) : (y))

static const char *TAG = "http_face_match";

static char g_access_token[100] = "";

/* Event handler for HTTP client events */
esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
    static char *output_buffer;  // Buffer to store response of http request from event handler
    static int output_len;       // Stores number of bytes read
    switch(evt->event_id) {
        case HTTP_EVENT_ERROR:
            ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
            break;
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:
            ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
            break;
        case HTTP_EVENT_ON_DATA:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
            /*
             *  Check for chunked encoding is added as the URL for chunked encoding used in this example returns binary data.
             *  However, event handler can also be used in case chunked encoding is used.
             */
            // If user_data buffer is configured, copy the response into the buffer
            int copy_len = 0;
            if (evt->user_data) {
                // The last byte in evt->user_data is kept for the NULL character in case of out-of-bound access.
                copy_len = MIN(evt->data_len, (MAX_HTTP_OUTPUT_BUFFER - output_len));
                if (copy_len) {
                    memcpy(evt->user_data + output_len, evt->data, copy_len);
                }
            } else {
            if (!esp_http_client_is_chunked_response(evt->client)) {
                int content_len = esp_http_client_get_content_length(evt->client);
                    if (output_buffer == NULL) {
                        // We initialize output_buffer with 0 because it is used by strlen() and similar functions therefore should be null terminated.
                        output_buffer = (char *) calloc(content_len + 1, sizeof(char));
                        output_len = 0;
                        if (output_buffer == NULL) {
                            ESP_LOGE(TAG, "Failed to allocate memory for output buffer");
                            return ESP_FAIL;
                        }
                    }
                    copy_len = MIN(evt->data_len, (content_len - output_len));
                    if (copy_len) {
                        memcpy(output_buffer + output_len, evt->data, copy_len);
                    }
                }
            }
            output_len += copy_len;
            break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
            if (output_buffer != NULL) {
                // Response is accumulated in output_buffer. Uncomment the below line to print the accumulated response
                // ESP_LOG_BUFFER_HEX(TAG, output_buffer, output_len);
                free(output_buffer);
                output_buffer = NULL;
            }
            output_len = 0;
            break;
        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
            int mbedtls_err = 0;
            esp_err_t err = esp_tls_get_and_clear_last_error((esp_tls_error_handle_t)evt->data, &mbedtls_err, NULL);
            if (err != 0) {
                ESP_LOGI(TAG, "Last esp error code: 0x%x", err);
                ESP_LOGI(TAG, "Last mbedtls failure: 0x%x", mbedtls_err);
            }
            if (output_buffer != NULL) {
                free(output_buffer);
                output_buffer = NULL;
            }
            output_len = 0;
            break;
        case HTTP_EVENT_REDIRECT:
            ESP_LOGD(TAG, "HTTP_EVENT_REDIRECT");
            esp_http_client_set_header(evt->client, "From", "user@example.com");
            esp_http_client_set_header(evt->client, "Accept", "text/html");
            esp_http_client_set_redirection(evt->client);
            break;
    }
    return ESP_OK;
}

static bool http_refresh_access_token(const char *client_id, const char *secret_key)
{
    if (client_id == NULL || secret_key == NULL) {
        ESP_LOGE(TAG, "Invalid client information");
        return false;
    }

    char *local_response_buffer = (char *)heap_caps_malloc(MAX_HTTP_OUTPUT_BUFFER * sizeof(char), MALLOC_CAP_SPIRAM);
    if (local_response_buffer == NULL) {
        ESP_LOGE(TAG, "Local response buffer malloc failed");
        return false;
    }

    /* Generate the URL based on the client_id and secret_key */
    char url[256];
    sprintf(url, "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s", client_id, secret_key);

    esp_http_client_config_t config = {
        .url = url,
        .event_handler = _http_event_handler,
        .user_data = local_response_buffer,
        .disable_auto_redirect = true,
        .crt_bundle_attach = esp_crt_bundle_attach,
    };

    esp_http_client_handle_t http_client = esp_http_client_init(&config);
    if (http_client == NULL) {
        ESP_LOGE(TAG, "HTTP client init failed");
        free(local_response_buffer);
        return false;
    }

    esp_err_t err = esp_http_client_perform(http_client);
    if (err == ESP_OK) {
        ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %"PRId64,
                esp_http_client_get_status_code(http_client),
                esp_http_client_get_content_length(http_client));
    } else {
        ESP_LOGE(TAG, "HTTP GET request failed: %s", esp_err_to_name(err));
        esp_http_client_cleanup(http_client);
        free(local_response_buffer);
        return false;
    }

    /* Parse the JSON response */
    cJSON *cjson_root = cJSON_Parse(local_response_buffer);

    strcpy(g_access_token, cJSON_GetObjectItem(cjson_root, "access_token")->valuestring);


    ESP_LOGI(TAG, "Access token: %s", g_access_token);

    cJSON_Delete(cjson_root);
    esp_http_client_cleanup(http_client);
    free(local_response_buffer);

    return true;
}

/* Parse the JSON response and extract the face info */
static double http_parse_score_id(const char *local_response_buffer, char *user_id)
{
    if (local_response_buffer == NULL) {
        ESP_LOGE(TAG, "NULL pointer input");
        return 0;
    }

    /* Parse the JSON response */
    cJSON *cjson_root = cJSON_Parse(local_response_buffer);

    /* Print the JSON response */
    char *json_string = cJSON_Print(cjson_root);
    ESP_LOGI(TAG, "Response result: %s", json_string);

    /* Check the error code, return if error code not 0 */
    int error_code;
    cJSON *error_code_item = cJSON_GetObjectItem(cjson_root, "error_code");
    if (error_code_item != NULL && cJSON_IsNumber(error_code_item)) {
        error_code = error_code_item->valueint;
    } else {
        goto err;
    }

    if (error_code != 0) {
        ESP_LOGE(TAG, "Error code: %d", error_code);
        goto err;
    }

    /* Extract the face info */
    cJSON *result = cJSON_GetObjectItem(cjson_root, "result");
    cJSON *user_list = cJSON_GetObjectItem(result, "user_list");
    cJSON *user_item = cJSON_GetArrayItem(user_list, 0);
    cJSON *score_item = cJSON_GetObjectItem(user_item, "score");

    /* Extract the score and user_id */
    double score = 0;
    if (score_item != NULL && cJSON_IsNumber(score_item)) {
        score = score_item->valuedouble;
    } else {
        goto err;
    }

    strcpy(user_id, cJSON_GetObjectItem(user_item, "user_id")->valuestring);

    /* Release the resource */
    cJSON_Delete(cjson_root);

    return score;

err:
    cJSON_Delete(cjson_root);
    return 0;
}

/* Perform an HTTP POST request to search face info */
bool http_face_match(char *user_id, uint8_t *frame_buffer, size_t frame_length)
{
    if (http_refresh_access_token(CONFIG_BAIDU_API_KEY, CONFIG_BAIDU_SECRET_KEY)) {
        ESP_LOGI(TAG, "Access token: %s", g_access_token);
    } else {
        ESP_LOGE(TAG, "Failed to get access token");
        return false;
    }

    /* 1. Create local response buffer for the HTTP response */
    char *local_response_buffer = (char *)heap_caps_malloc(MAX_HTTP_OUTPUT_BUFFER * sizeof(char), MALLOC_CAP_SPIRAM);

    if (local_response_buffer == NULL) {
        ESP_LOGE(TAG, "Local response buffer calloc failed");
        return false;
    }

    /* 2. Convert the JPEG data to base64 */
    size_t output_base64_length;
    size_t output_length = frame_length * 2;
    unsigned char *output_base64 = (unsigned char *)heap_caps_malloc(output_length, MALLOC_CAP_SPIRAM);

    if (output_base64 == NULL) {
        printf("Failed to allocate memory for base64 output\n");
        return false;
    }

    mbedtls_base64_encode(output_base64, output_length, &output_base64_length, frame_buffer, frame_length);
    // ESP_LOGI(TAG, "Base64 data: %s", output_base64);

    /* 3. Configure the HTTP client with the URL and event handler */
    char url[256]; // Adjust the size as needed
    sprintf(url, "https://aip.baidubce.com/rest/2.0/face/v3/search?access_token=%s", g_access_token);

    esp_http_client_config_t config = {
        .url                    = url,
        .event_handler          = _http_event_handler,
        .user_data              = local_response_buffer,        // Pass address of local buffer to get response
        .disable_auto_redirect  = true,
        .crt_bundle_attach      = esp_crt_bundle_attach,
    };

    esp_http_client_handle_t http_client = esp_http_client_init(&config);
    if (http_client == NULL) {
        ESP_LOGE(TAG, "HTTP client init failed");
        goto err;
    }

    /* 4. Set HTTP POST header and body, then send HTTP POST
    * Note: The post_data_length should be set according to the frame buffer size
    */
    size_t post_data_length = 50 * 1024;
    char *post_data = (char *)heap_caps_malloc(post_data_length * sizeof(char), MALLOC_CAP_SPIRAM);
    if (post_data == NULL) {
        ESP_LOGE(TAG, "Failed to allocate memory for post data");
        goto err;
    }

    /* Note: Please update the group_id_list to match your own group ID */
    snprintf(post_data, post_data_length, "{\"image\":\"%s\",\"image_type\":\"BASE64\",\"group_id_list\":\"esp_frineds\"}", output_base64);
    // ESP_LOGI(TAG, "Post data: %s", post_data);

    esp_http_client_set_method(http_client, HTTP_METHOD_POST);
    esp_http_client_set_header(http_client, "Content-Type", "application/json");
    esp_http_client_set_post_field(http_client, post_data, strlen(post_data));
    esp_err_t err = esp_http_client_perform(http_client);
    if (err == ESP_OK) {
        ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %"PRId64,
                esp_http_client_get_status_code(http_client),
                esp_http_client_get_content_length(http_client));
    } else {
        ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err));
        esp_http_client_cleanup(http_client);
        free(post_data);
        goto err;
    }

    /* 5. Determine face matches based on the response score */
    bool result = false;

    if (http_parse_score_id(local_response_buffer, user_id) > 75) {
        ESP_LOGI(TAG, "Face matched");
        ESP_LOGI(TAG, "User ID: %s", user_id);
        result = true;
    } else {
        ESP_LOGI(TAG, "Face not matched");
        user_id = "Unknown";
        result = false;
    }

    /* 6. Release the resource */
    esp_http_client_cleanup(http_client);
    free(local_response_buffer);
    free(output_base64);
    free(post_data);

    return result;

err:
    free(local_response_buffer);
    free(output_base64);
    return false;
}